﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Reflection;
using Microsoft.Practices.EnterpriseLibrary.Logging;
using System.Diagnostics;
using System.IO;
using System.Collections;

namespace Logging
{
    public  class SynkronLogger
    {
        public   const string InvokingAssemblyNameKey = "invoking assembly";

        #region Constructors

        public SynkronLogger(string category)
            : this(new string[] { category }, Assembly.GetCallingAssembly())
        {
        }

        public SynkronLogger(string[] categories)
            : this(categories, Assembly.GetCallingAssembly())
        {
        }

        public SynkronLogger(string[] categories, Assembly invokingAssembly)
        {
            if (categories == null)
                throw new ArgumentNullException("categories");
            if (invokingAssembly == null)
                throw new ArgumentNullException("invokingAssembly");

            this.Categories = categories;
            AssemblyName assemblyName = invokingAssembly.GetName();
            if (assemblyName != null)
                this.InvokingAssemblyName = assemblyName.Name;
        }

        public SynkronLogger(string[] categories, string invokingAssemblyName)
        {
            if (categories == null)
                throw new ArgumentNullException("categories");
            if (invokingAssemblyName == null)
                throw new ArgumentNullException("invokingAssemblyName");

            this.Categories = categories;
            this.InvokingAssemblyName = invokingAssemblyName;
        }

        #endregion

        #region Properties

        /// <summary>
        /// The name of the assembly to be included in log properties
        /// </summary>
        public string InvokingAssemblyName
        {
            get;
            private set;
        }

        /// <summary>
        /// Categories to be set on each log message
        /// </summary>
        public string[] Categories { get; private set; }

        /// <summary>
        /// Properties to be added to every log created
        /// </summary>
        /// <remarks>
        /// Modifying the Dictionary is not thread-safe. In cases where it must be modified
        /// either arrange for exclusive access or make a copy of the Dictionary, modify
        /// it and set it on this property.
        /// </remarks>
        public IDictionary<string, object> LogProperties { get; set; }

        #endregion

        #region Exception related

        // these constants are the keys under which information about an exception is
        // added to the Dictionary. If there is a base (ultimate inner) exception then
        // it's details are recorded separately from the outer exception's
        public const string OuterExceptionMessage = "oExMessage";
        public const string OuterExceptionClassname = "oExClassname";
        public const string OuterExceptionMethod = "oExMethod";
        public const string OuterExceptionMethodClassname = "oExMethodClass";
        public const string OuterExceptionStackTrace = "oExStackTrace";
        public const string OuterExceptionI2SId = "oExI2SId";
        public const string OuterExceptionFaultException = "oExFaultException";

        public const string BaseExceptionMessage = "bExMessage";
        public const string BaseExceptionClassname = "bExClassname";
        public const string BaseExceptionMethod = "bExMethod";
        public const string BaseExceptionMethodClassname = "bExMethodClass";
        public const string BaseExceptionStackTrace = "bExStackTrace";
        public const string BaseExceptionI2SId = "bExI2SId";
        public const string BaseExceptionFaultException = "bExFaultException";

        public const string InnerExceptionMessage = "iExMessage";
        public const string InnerExceptionClassname = "iExClassname";
        public const string InnerExceptionMethod = "iExMethod";
        public const string InnerExceptionMethodClassname = "iExMethodClass";
        public const string InnerExceptionStackTrace = "iExStackTrace";
        public const string InnerExceptionI2SId = "iExI2SId";
        public const string InnerExceptionFaultException = "iExFaultException";

        internal class ExceptionPartDictionaryKeys
        {
            internal string ExceptionMessage;
            internal string ExceptionClassname;
            internal string ExceptionMethod;
            internal string ExceptionMethodClassname;
            internal string ExceptionStackTrace;
            internal string ExceptionI2SId;
            internal string ExceptionFaultException;
        }

        private static ExceptionPartDictionaryKeys outerExceptionKeys = new ExceptionPartDictionaryKeys()
        {
            ExceptionMessage = OuterExceptionMessage,
            ExceptionClassname = OuterExceptionClassname,
            ExceptionMethod = OuterExceptionMethod,
            ExceptionMethodClassname = OuterExceptionMethodClassname,
            ExceptionStackTrace = OuterExceptionStackTrace,
            ExceptionI2SId = OuterExceptionI2SId,
            ExceptionFaultException = OuterExceptionFaultException
        };

        private static ExceptionPartDictionaryKeys innerExceptionKeys = new ExceptionPartDictionaryKeys()
        {
            ExceptionMessage = InnerExceptionMessage,
            ExceptionClassname = InnerExceptionClassname,
            ExceptionMethod = InnerExceptionMethod,
            ExceptionMethodClassname = InnerExceptionMethodClassname,
            ExceptionStackTrace = InnerExceptionStackTrace,
            ExceptionI2SId = InnerExceptionI2SId,
            ExceptionFaultException = InnerExceptionFaultException
        };

        private static ExceptionPartDictionaryKeys baseExceptionKeys = new ExceptionPartDictionaryKeys()
        {
            ExceptionMessage = BaseExceptionMessage,
            ExceptionClassname = BaseExceptionClassname,
            ExceptionMethod = BaseExceptionMethod,
            ExceptionMethodClassname = BaseExceptionMethodClassname,
            ExceptionStackTrace = BaseExceptionStackTrace,
            ExceptionI2SId = BaseExceptionI2SId,
        };

        /// <summary>
        /// Extracts the standard information from an Exception and stores it in a Dictionary
        /// </summary>
        /// <remarks>
        /// For automatic processing of exception information it is not enough that it be
        /// present in the message since there is no easy way to consistently extract the
        /// information back out of the message. The solution is to extract the information that
        /// may be used for automatic processing and place it in a Dictionary with
        /// consistent keys.
        /// 
        /// The information extracts consists of
        /// 1. The type name of the exception
        /// 2. The Source of the exception
        /// 3. The TargetSite (i.e. method that threw the exception)
        /// 4. If this is an 
        /// </remarks>
        /// <param name="ex"></param>
        /// <returns></returns>
        public virtual void AddPropertiesFromException(IDictionary<string, object> dictionary, Exception ex)
        {
            if (ex == null)
                return;

            AddExceptionInfoToDictionary(dictionary,
                                        ex,
                                        outerExceptionKeys,
                                        "o_");

            Exception baseException = ex.GetBaseException();
            if (baseException != null && !object.ReferenceEquals(ex, baseException))
            {
                AddExceptionInfoToDictionary(dictionary,
                                            baseException,
                                            baseExceptionKeys,
                                            "b_");
            }
            else
            {
                Exception innerException = ex.InnerException;
                if (innerException != null && !object.ReferenceEquals(ex, innerException) && !object.ReferenceEquals(baseException, innerException))
                {
                    AddExceptionInfoToDictionary(dictionary,
                                                innerException,
                                                innerExceptionKeys,
                                                "i_");
                }
            }
        }

        public virtual Dictionary<string, object> ExtractPropertiesFromException(Exception ex)
        {
            if (ex == null)
                return null;

            Dictionary<string, object> retval = new Dictionary<string, object>();
            AddPropertiesFromException(retval, ex);
            return retval;
        }

        /// <summary>
        /// Extract the information from an exception and store it in the dictionary
        /// </summary>
        /// <param name="dictionary"></param>
        /// <param name="ex"></param>
        /// <param name="exceptionKeys"></param>
        /// <param name="exDataKeyPrefix">The string to be prefixed to each key from ex.Data. This
        /// prefix can distinguish between the inner and outer exceptions.</param>
        private static void AddExceptionInfoToDictionary(IDictionary<string, object> dictionary,
            Exception ex,
            ExceptionPartDictionaryKeys exceptionKeys,
            string exDataKeyPrefix)
        {
            ConditionallyAddItem(dictionary, exceptionKeys.ExceptionMessage, ex.Message);
            ConditionallyAddItem(dictionary, exceptionKeys.ExceptionStackTrace, ex.StackTrace);
            ConditionallyAddItem(dictionary, exceptionKeys.ExceptionClassname, ex.GetType().FullName);
            MethodBase mb = ex.TargetSite;
            if (mb != null)
            {
                ConditionallyAddItem(dictionary, exceptionKeys.ExceptionMethod, mb.Name);
                if (mb.DeclaringType != null)
                    ConditionallyAddItem(dictionary, exceptionKeys.ExceptionMethodClassname, mb.DeclaringType.FullName);
            }

            if (ex is SynkronException)
            {
                ConditionallyAddItem(dictionary, exceptionKeys.ExceptionI2SId, ((SynkronException)ex).ID);
            }

            if (ex is System.ServiceModel.FaultException)
            {
                ConditionallyAddItem(dictionary, exceptionKeys.ExceptionFaultException, ex.ToString());
            }


            if (ex.Data != null)
            {
                foreach (DictionaryEntry ent in ex.Data)
                {
                    if (ent.Key is string)
                        ConditionallyAddItem(dictionary, exDataKeyPrefix + (string)ent.Key, ent.Value);
                }
            }
        }



        private static void ConditionallyAddItem(IDictionary<string, object> dictionary,
            string keyname,
            object val)
        {
            if (val == null
                || (val is string && string.IsNullOrEmpty((string)val))
                || dictionary == null
                || string.IsNullOrEmpty(keyname))
                return;

            dictionary[keyname] = val;
        }

        #endregion


        /// <summary>
        /// Create a LogEntry from the combined information in the parameters and this instance
        /// </summary>
        /// <param name="message">The message to set in the LogEntry.
        /// This may either be a Func&lt;string&gt; in which case it is called, or an object
        /// in which case its ToString() method will be called.
        /// 
        /// No exception will be thrown
        /// for a null value. This allows the caller to perform a ShouldLog() test before going
        /// to the trouble of building the message string.</param>
        /// <param name="eventId"></param>
        /// <param name="severity"></param>
        /// <param name="priority"></param>
        /// <param name="ex">An optional exception from which information will be extracted
        /// and added to the LogEntry properties</param>
        /// <param name="properties"></param>
        /// <returns></returns>
        public LogEntry CreateLogEntry(int eventId,
            TraceEventType severity,
            int priority,
            object message,
            Exception ex,
            IDictionary<string, Object> properties)
        {
            LogEntry logEntry = CreateBaseLogEntry(eventId, severity, priority);
            AddAdditionalInformation(logEntry, message, ex, properties);
            return logEntry;
        }

        /// <summary>
        /// Creates a LogEntry populated only with the information necessary to determine
        /// if the message should be logged. The additional information can be added by a
        /// call to AddAdditionalInformation().
        /// </summary>
        /// <param name="eventId"></param>
        /// <param name="severity"></param>
        /// <param name="priority"></param>
        /// <returns></returns>
        public LogEntry CreateBaseLogEntry(int eventId,
            TraceEventType severity,
            int priority)
        {
            LogEntry logEntry = new LogEntry()
            {
                Categories = this.Categories,
                EventId = eventId,
                ExtendedProperties = this.LogProperties,
                Priority = priority,
                Severity = severity
            };

            return logEntry;
        }

        /// <summary>
        /// If the parameters indicate a LogEntry that should be logged, according to
        /// the logging configuration, then return a LogEntry containing the information
        /// from the parameters than may then be fully populated by a call to
        /// AddAdditionalInformation().
        /// </summary>
        /// <param name="eventId"></param>
        /// <param name="severity"></param>
        /// <param name="priority"></param>
        /// <returns>Null if no log should be recorded for these parameters, a non-null
        /// value if the LogEntry should be fully populated and then output.</returns>
        public LogEntry ConditionalCreateBaseLogEntry(int eventId,
            TraceEventType severity,
            int priority)
        {
            LogEntry logEntry = CreateBaseLogEntry(eventId, severity, priority);
            if (Logger.ShouldLog(logEntry))
                return logEntry;
            else
                return null;
        }

        public LogEntry ConditionalCreateBaseLogEntry(int eventId,
            TraceEventType severity)
        {
            return this.ConditionalCreateBaseLogEntry(eventId, severity, -1);
        }

        /// <summary>
        /// Called to add additional information to a LogEntry created by a call to CreateBaseLogEntry() or ConditionalCreateBaseLogEntry()
        /// </summary>
        /// <param name="logEntry"></param>
        /// <param name="message">Optional. Either an object on which ToString() can be called
        /// or a Func&lt;string&gt; that will be invoked</string></param>
        /// <param name="ex"></param>
        /// <param name="properties"></param>
        public void AddAdditionalInformation(LogEntry logEntry,
            object message,
            Exception ex,
            IDictionary<string, Object> properties)
        {
            if (logEntry == null)
                throw new ArgumentNullException("logEntry");

            if (ex != null || properties != null)
            {
                Dictionary<string, object> exceptionProperties = (ex != null) ? this.ExtractPropertiesFromException(ex) : null;
                logEntry.ExtendedProperties = MergeDictionaries(logEntry.ExtendedProperties, exceptionProperties, properties);
            }
            else
            {
                Dictionary<string, object> alwaysProperties = new Dictionary<string, object>();
                AddInstanceProperties(alwaysProperties);
                if (alwaysProperties.Count > 0)
                    logEntry.ExtendedProperties = alwaysProperties;
            }

            if (message != null)
            {
                // in most cases message will be a string so save time
                // by just assigning it rather than calling ToString() on it.
                if (message is string)
                    logEntry.Message = (string)message;
                else if (message is Func<string>)
                    logEntry.Message = ((Func<string>)message)();
                else
                    logEntry.Message = message.ToString();
            }
        }

        public void AddAdditionalInformationAndWrite(LogEntry logEntry,
            object message,
            Exception ex,
            IDictionary<string, Object> properties)
        {
            this.AddAdditionalInformation(logEntry, message, ex, properties);
            Logger.Write(logEntry);
        }



        public LogEntry CreateLogEntry(int eventId,
            TraceEventType severity,
            object message,
            IDictionary<string, Object> properties)
        {
            return CreateLogEntry(eventId, severity, -1, message, null, properties);
        }

        public LogEntry CreateLogEntry(int eventId,
            TraceEventType severity,
            object message)
        {
            return CreateLogEntry(eventId, severity, -1, message, null, null);
        }


        /// <summary>
        /// Combines the contents of the dictionaries and adds additional information
        /// </summary>
        /// <remarks>
        /// The additional information is variable since it is supplied by the virtual
        /// AddInstanceProperties() method that may be overridden by subclasses.
        /// </remarks>
        /// <param name="initial">The initial values to be merged. If any entries have a key
        /// shared by an entry in the additions parameter then the additions entry will take
        /// precedence. If this parameter is a proper Dictionary then its Comparer will
        /// be used in the result.</param>
        /// <param name="additions">The values to be added to the initial values. If entries
        /// in the two dictionaries share the same key then the one in this parameter will
        /// take precedence and be returned in the result.</param>
        /// <returns>The result of merging the two dictionaries.</returns>
        private IDictionary<string, object> MergeDictionaries(params IDictionary<string, object>[] dictionaries)
        {
            Dictionary<string, object> retval = null;
            foreach (IDictionary<string, object> dictionary in dictionaries)
            {
                if (dictionary == null)
                    continue;

                // create a new dictionary to contain the union of the two dictionaries. Load it with
                // the instanceProperties first so that any entry in properties with the same key will
                // override the corresponding entry in instanceProperties.
                if (retval == null)
                {
                    if (dictionary is Dictionary<string, object>)
                        retval = new Dictionary<string, object>(dictionary, ((Dictionary<string, object>)dictionary).Comparer);
                    else
                        retval = new Dictionary<string, object>(dictionary);
                }
                else
                {
                    foreach (KeyValuePair<string, object> propEntry in dictionary)
                    {
                        retval[propEntry.Key] = propEntry.Value;
                    }
                }
            }

            if (retval == null)
                retval = new Dictionary<string, object>();

            // add any special entries, currently just the invoking Assembly name
            AddInstanceProperties(retval);

            return retval;
        }

        /// <summary>
        /// Adds the properties that all logs created via this instance should have set on them.
        /// </summary>
        /// <remarks>
        /// In circumstances where code wishes to add other instance-specific properties
        /// create a subclass and override this method.
        /// </remarks>
        /// <param name="properties"></param>
        protected virtual void AddInstanceProperties(IDictionary<string, object> properties)
        {
            ConditionallyAddItem(properties, InvokingAssemblyNameKey, this.InvokingAssemblyName);
        }


        public void Write(int eventId,
            TraceEventType severity,
            int priority,
            object message,
            Exception ex,
            IDictionary<string, Object> properties)
        {
            if (message == null)
                throw new ArgumentNullException("message");

            LogEntry logEntry = ConditionalCreateBaseLogEntry(eventId, severity, priority);
            if (logEntry == null)
                return;

            this.AddAdditionalInformationAndWrite(logEntry, message, ex, properties);
        }

        public void Write(int eventId,
            TraceEventType severity,
            int priority,
            object message,
            IDictionary<string, Object> properties)
        {
            Write(eventId, severity, priority, message, null, properties);
        }

        public void Write(int eventID,
            string msg,
            System.Diagnostics.TraceEventType severity,
            Exception ex)
        {
            Write(eventID, severity, -1, msg, ex, null);
        }

        public void Write(int eventId,
            TraceEventType severity,
            int priority,
            Func<string> message,
            IDictionary<string, Object> properties)
        {
            Write(eventId, severity, priority, (object)message, properties);
        }



        public void Write(int eventId,
            TraceEventType severity,
            object message)
        {
            Write(eventId, severity, -1, message, null);
        }

        public void Write(int eventId,
            TraceEventType severity,
            Func<string> message)
        {
            Write(eventId, severity, -1, (object)message, null);
        }

        public void WriteCritical(int eventId, object message)
        {
            Write(eventId, TraceEventType.Critical, -1, message, null);
        }

        public void WriteError(int eventId, object message)
        {
            Write(eventId, TraceEventType.Error, -1, message, null);
        }

        public void WriteWarning(int eventId, object message)
        {
            Write(eventId, TraceEventType.Warning, -1, message, null);
        }

        public void WriteInformation(int eventId, object message)
        {
            Write(eventId, TraceEventType.Information, -1, message, null);
        }

        public void WriteVerbose(int eventId, object message)
        {
            Write(eventId, TraceEventType.Verbose, -1, message, null);
        }



        public void WriteCritical(int eventId, object message, IDictionary<string, Object> properties)
        {
            Write(eventId, TraceEventType.Critical, -1, message, properties);
        }

        public void WriteError(int eventId, object message, IDictionary<string, Object> properties)
        {
            Write(eventId, TraceEventType.Error, -1, message, properties);
        }

        public void WriteWarning(int eventId, object message, IDictionary<string, Object> properties)
        {
            Write(eventId, TraceEventType.Warning, -1, message, properties);
        }

        public void WriteInformation(int eventId, object message, IDictionary<string, Object> properties)
        {
            Write(eventId, TraceEventType.Information, -1, message, properties);
        }

        public void WriteVerbose(int eventId, object message, IDictionary<string, Object> properties)
        {
            Write(eventId, TraceEventType.Verbose, -1, message, properties);
        }



        public void WriteCritical(int eventId, Func<string> message, IDictionary<string, Object> properties)
        {
            Write(eventId, TraceEventType.Critical, -1, message, properties);
        }

        public void WriteError(int eventId, Func<string> message, IDictionary<string, Object> properties)
        {
            Write(eventId, TraceEventType.Error, -1, message, properties);
        }

        public void WriteWarning(int eventId, Func<string> message, IDictionary<string, Object> properties)
        {
            Write(eventId, TraceEventType.Warning, -1, message, properties);
        }

        public void WriteInformation(int eventId, Func<string> message, IDictionary<string, Object> properties)
        {
            Write(eventId, TraceEventType.Information, -1, message, properties);
        }

        public void WriteVerbose(int eventId, Func<string> message, IDictionary<string, Object> properties)
        {
            Write(eventId, TraceEventType.Verbose, -1, message, properties);
        }




        public void WriteCritical(int eventId, Func<string> message, Exception ex, IDictionary<string, Object> properties)
        {
            Write(eventId, TraceEventType.Critical, -1, message, ex, properties);
        }

        public void WriteError(int eventId, Func<string> message, Exception ex, IDictionary<string, Object> properties)
        {
            Write(eventId, TraceEventType.Error, -1, message, ex, properties);
        }

        public void WriteWarning(int eventId, Func<string> message, Exception ex, IDictionary<string, Object> properties)
        {
            Write(eventId, TraceEventType.Warning, -1, message, ex, properties);
        }

        public void WriteInformation(int eventId, Func<string> message, Exception ex, IDictionary<string, Object> properties)
        {
            Write(eventId, TraceEventType.Information, -1, message, ex, properties);
        }

        public void WriteVerbose(int eventId, Func<string> message, Exception ex, IDictionary<string, Object> properties)
        {
            Write(eventId, TraceEventType.Verbose, -1, message, ex, properties);
        }



        public void WriteCritical(int eventId, object message, Exception ex, IDictionary<string, Object> properties)
        {
            Write(eventId, TraceEventType.Critical, -1, message, ex, properties);
        }

        public void WriteError(int eventId, object message, Exception ex, IDictionary<string, Object> properties)
        {
            Write(eventId, TraceEventType.Error, -1, message, ex, properties);
        }

        public void WriteWarning(int eventId, object message, Exception ex, IDictionary<string, Object> properties)
        {
            Write(eventId, TraceEventType.Warning, -1, message, ex, properties);
        }

        public void WriteInformation(int eventId, object message, Exception ex, IDictionary<string, Object> properties)
        {
            Write(eventId, TraceEventType.Information, -1, message, ex, properties);
        }

        public void WriteVerbose(int eventId, object message, Exception ex, IDictionary<string, Object> properties)
        {
            Write(eventId, TraceEventType.Verbose, -1, message, ex, properties);
        }



        public void WriteCritical(int eventId, object message, Exception ex)
        {
            Write(eventId, TraceEventType.Critical, -1, message, ex, null);
        }

        public void WriteError(int eventId, object message, Exception ex)
        {
            Write(eventId, TraceEventType.Error, -1, message, ex, null);
        }

        public void WriteWarning(int eventId, object message, Exception ex)
        {
            Write(eventId, TraceEventType.Warning, -1, message, ex, null);
        }

        public void WriteInformation(int eventId, object message, Exception ex)
        {
            Write(eventId, TraceEventType.Information, -1, message, ex, null);
        }

        public void WriteVerbose(int eventId, object message, Exception ex)
        {
            Write(eventId, TraceEventType.Verbose, -1, message, ex, null);
        }

        public string Desc(object obj) // To Refactor Later For Writing to Text Value of Objects
        {
            string s = "";
            foreach (System.ComponentModel.PropertyDescriptor descriptor in System.ComponentModel.TypeDescriptor.GetProperties(obj))
            {
                string name = descriptor.Name;
                object value = descriptor.GetValue(obj);
                if (value == null)
                {
                    value = "";
                }
                s += name + " : " + value.ToString() + "" + System.Environment.NewLine;

            }
            return s;
        }

    }
}